<!doctype html>
<html>
  <body>
    <script src="three.js"></script>
    <script src="fakeXrDisplay.js"></script>
    <script src="keycode.js"></script>
    <script src="svg-boundings.js"></script>
    <script src="sprite3d.js"></script>
    <script src="skin.js"></script>
    <script src="xrmp.js"></script>
    <script src="xrmp-three.js"></script>
    <script>
let session, fakeXrDisplay, tabs = [], tabId = 0, xrmp, lastPresseds = [false, false], lastAxes = [[0.5, 0.5], [0.5, 0.5]], lastPadToucheds = [false, false], scrollFactors = [0, 0], scaleFactors = [1, 1];

const links = [
  {
    name: 'Kitchen sink',
    url: 'hello_ml.html',
  },
  {
    name: 'Tutorial',
    url: 'tutorial.html',
  },
  {
    name: 'Exobot',
    url: 'exobot.html',
  },
  {
    name: 'Meshing',
    url: 'meshing_ml.html',
  },
  {
    name: 'Planes',
    url: 'planes_ml.html',
  },
  {
    name: 'Paint',
    url: 'paint_ml.html',
  },
  {
    name: 'Graffiti',
    url: 'graffiti_ml.html',
  },
  {
    name: 'Hands',
    url: 'hands_ml.html',
  },
  {
    name: 'Minimap',
    url: 'minimap_ml.html',
  },
  {
    name: 'Avatar',
    url: 'avatar_ml.html',
  },
  {
    name: 'Shooter',
    url: 'shooter_ml.html',
  },
  {
    name: 'Sword',
    url: 'sword_ml.html',
  },
  {
    name: 'Bow',
    url: 'bow_ml.html',
  },
  {
    name: 'Pathfinding',
    url: 'pathfinding_ml.html',
  },
  {
    name: 'Radar',
    url: 'radar_ml.html',
  },
  {
    name: 'Image tracking',
    url: 'imagetracking_ml.html',
  },
  {
    name: 'Microphone',
    url: 'microphone.html',
  },
].map(({name, url}) => ({name, url, iconMesh: null}));
let servers = [];
let serversLoading = false;
let serverConnectedUrl = null;

const DEFAULT_URL = 'https://aframe.io/';
const DEFAULT_REGISTRY_URL = 'http://xrmp.exokit.org:9001';
const DEFAULT_SKIN_URL = 'skin2.png';
const RAY_COLOR = 0x44c2ff;
const RAY_HIGHLIGHT_COLOR = new THREE.Color(RAY_COLOR).multiplyScalar(0.5).getHex();
const rayDistance = 3;
const menuWidth = 1024;
const menuHeight = menuWidth * 0.1;
const menuWorldWidth = 1;
const menuWorldHeight = menuWorldWidth / menuWidth * menuHeight;
const menuPositionHeight = 0.3;
const urlBarWidth = menuWorldWidth;
const urlBarHeight = menuHeight;
const urlBarOffset = urlBarHeight;
const fontFamily = 'Arial';
// const fontFamily = 'monospace';
const fontSize = 60;
const keyboardWidth = 2048;
const keyboardHeight = 716;
const keyboardMatrix = [keyboardWidth / 963.266, keyboardHeight / 337.215];
const cursorWidth = 4;
const armQuaternionOffset = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, -1, 0), new THREE.Vector3(0, 0, -1));

const localVector = new THREE.Vector3();
const localVector2 = new THREE.Vector3();
const localVector3 = new THREE.Vector3();
const localQuaternion = new THREE.Quaternion();
const localQuaternion2 = new THREE.Quaternion();
const localEuler = new THREE.Euler(0, 0, 0, 'YXZ');
const localEuler2 = new THREE.Euler(0, 0, 0, 'YXZ');
const localMatrix = new THREE.Matrix4();

function parseQuery(s) {
  var query = {};
  var pairs = (s[0] === '?' ? s.substr(1) : s).split('&');
  for (var i = 0; i < pairs.length; i++) {
    var pair = pairs[i].split('=');
    query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
  }
  return query;
}
const query = parseQuery(window.location.search);
const registryUrl = query.registryUrl || DEFAULT_REGISTRY_URL;

// load

const keyMap = [];
fetch('keyboard.svg')
  .then(res => res.text())
  .then(keyboardText => {
    const div = document.createElement('div');
    div.innerHTML = keyboardText;
    const keyEls = div.querySelectorAll('svg > g[key]');
    for (let i = 0; i < keyEls.length; i++) {
      const keyEl = keyEls[i];
      const key = keyEl.getAttribute('key');
      const shapeEl = keyEl.children[0];
      const boundings = svgBoundings[shapeEl.tagName.toLowerCase()](shapeEl);
      const {
        left,
        right,
        top,
        bottom,
      } = boundings;
      const x1 = left * keyboardMatrix[0];
      const x2 = right * keyboardMatrix[0];
      const y1 = top * keyboardMatrix[1];
      const y2 = bottom * keyboardMatrix[1];
      keyMap.push([key, x1, y1, x2, y2]);
    }
  })
  .catch(err => {
    console.warn(err.stack);
  });

let keyboardHighlightCanvasCtx = null;
new Promise((accept, reject) => {
  const img = new Image();
  img.crossOrigin = 'Anonymous';
  img.src = 'keyboard-highlight.png';
  img.onload = () => {
    const canvas = document.createElement('canvas');
    canvas.width = img.naturalWidth;
    canvas.height = img.naturalHeight;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    accept(ctx);
  };
  img.onerror = err => {
    reject(err);
  };
})
  .then(ctx => {
    keyboardHighlightCanvasCtx = ctx;
  })
  .catch(err => {
    console.warn(err.stack);
  });

const iconMeshes = [];

const _requestSpriteMesh = url => new Promise((accept, reject) => {
  const img = new Image();
  img.crossOrigin = 'Anonymous';
  img.src = url;
  img.onload = () => {
    const spriteMesh = sprite3d.makeSpriteMesh(img);
    accept(spriteMesh);
  };
  img.onerror = err => {
    reject(err);
  };
});

const _makeBoxGeometry = (x = 1, y = 1, z = 1) => {
  const size = 1;
  const width = 0.005;

  const lineGeometry = new THREE.CylinderBufferGeometry(width, width, size, 3, 1);
  const geometry = new THREE.BufferGeometry();
  const positions = new Float32Array(lineGeometry.attributes.position.array.length * 12);
  geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
  // axis
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, y, 1)
      )
      .applyMatrix(
        localMatrix.makeTranslation(-size/2, 0, -size/2)
      )
      .attributes.position.array,
    lineGeometry.attributes.position.array.length * 0
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, y, 1)
      )
      .applyMatrix(
        localMatrix.makeTranslation(size/2, 0, -size/2)
      )
      .attributes.position.array,
    lineGeometry.attributes.position.array.length * 1
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, y, 1)
      )
      .applyMatrix(
        localMatrix.makeTranslation(-size/2, 0, size/2)
      )
      .attributes.position.array,
    lineGeometry.attributes.position.array.length * 2
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, y, 1)
      )
      .applyMatrix(
        localMatrix.makeTranslation(size/2, 0, size/2)
      )
      .attributes.position.array,
    lineGeometry.attributes.position.array.length * 3
  );
  // axis
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(x, 1, 1)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(0, -size/2, -size/2)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 4
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(x, 1, 1)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(0, -size/2, size/2)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 5
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(x, 1, 1)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(0, size/2, -size/2)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 6
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(x, 1, 1)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(0, 0, 1), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(0, size/2, size/2)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 7
  );
  // axis
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, 1, z)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(-size/2, -size/2, 0)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 8
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, 1, z)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(-size/2, size/2, 0)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 9
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, 1, z)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(size/2, -size/2, 0)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 10
  );
  positions.set(
    lineGeometry.clone()
      .applyMatrix(
        localMatrix.makeScale(1, 1, z)
      )
      .applyMatrix(
        localMatrix.makeRotationFromQuaternion(localQuaternion.setFromAxisAngle(localVector.set(1, 0, 0), Math.PI/2))
      )
      .applyMatrix(
        localMatrix.makeTranslation(size/2, size/2, 0)
      ).attributes.position.array,
    lineGeometry.attributes.position.array.length * 11
  );
  const numLinePositions = lineGeometry.attributes.position.array.length / 3;
  const indices = new Uint16Array(lineGeometry.index.array.length * 12);
  for (let i = 0; i < 12; i++) {
    indices.set(
      lineGeometry.index.array,
      lineGeometry.index.array.length * i
    );

    for (let j = 0; j < lineGeometry.index.array.length; j++) {
      lineGeometry.index.array[j] += numLinePositions;
    }
  }
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));
  return geometry;
};
const boxMaterial = new THREE.MeshPhongMaterial({
  color: 0x333333,
});
const _makeBoxMesh = () => {
  const geometry = _makeBoxGeometry();
  const material = boxMaterial;
  const mesh = new THREE.Mesh(geometry, material);
  mesh.visible = false;
  mesh.frustumCulled = false;
  return mesh;
};

const _mod = (v, d) => {
  const n = v % d;
  return n < 0 ? (d + n) : n;
};
const _angleDiff = (a, b) => _mod((b - a) + Math.PI, Math.PI * 2) - Math.PI;
const _makeSkinMesh = () => {
  const object = {};
  let onupdate = null;
  const uniforms = THREE.UniformsUtils.clone(skin.SKIN_SHADER.uniforms);
  object.setSkinUrl = skinUrl => {
    if (skinUrl) {
      const mesh = skin({
        limbs: true,
      });
      mesh.frustumCulled = false;

      mesh.onBeforeRender = (onBeforeRender => function() {
        mesh.material.uniforms.headRotation.value.copy(uniforms.headRotation.value);
        mesh.material.uniforms.leftArmRotation.value.copy(uniforms.leftArmRotation.value);
        mesh.material.uniforms.rightArmRotation.value.copy(uniforms.rightArmRotation.value);
        mesh.material.uniforms.theta.value = uniforms.theta.value;
        mesh.material.uniforms.headVisible.value = uniforms.headVisible.value;
        mesh.material.uniforms.hit.value = uniforms.hit.value;

        onBeforeRender.apply(this, arguments);
      })(mesh.onBeforeRender);

      return new Promise((accept, reject) => {
        const skinImg = new Image();
        skinImg.crossOrigin = 'Anonymous';
        skinImg.src = skinUrl;
        skinImg.onload = () => {
          accept(skinImg);
        };
        skinImg.onerror = err => {
          reject(err);
        };
      })
        .then(skinImg => {
          mesh.setImage(skinImg);

          onupdate = (hmd, gamepads) => {
            const hmdEuler = localEuler.setFromQuaternion(hmd.quaternion, localEuler.order);
            const playerEuler = localEuler2.setFromQuaternion(mesh.quaternion, localEuler2.order);
            const angleDiff = _angleDiff(hmdEuler.y, playerEuler.y);
            const angleDiffAbs = Math.abs(angleDiff);
            if (angleDiffAbs > Math.PI / 2) {
              playerEuler.y += (angleDiffAbs - (Math.PI / 2)) * (angleDiff < 0 ? 1 : -1);
              mesh.quaternion.setFromEuler(playerEuler);
            }

            const oldWorldPosition = mesh.getWorldPosition(localVector);
            mesh.position.copy(hmd.position)
              .sub(mesh.eye.getWorldPosition(localVector2))
              .add(oldWorldPosition);

            const playerQuaternionInverse = localQuaternion.copy(mesh.quaternion).inverse();
            mesh.head.quaternion.copy(playerQuaternionInverse).multiply(hmd.quaternion);
            mesh.updateMatrixWorld();

            const headQuaternionInverse = localQuaternion2.copy(mesh.head.quaternion).inverse();
            uniforms.headRotation.value.set(headQuaternionInverse.x, headQuaternionInverse.y, headQuaternionInverse.z, headQuaternionInverse.w);

            for (let i = 0; i < 2; i++) {
              const armRotation = uniforms[i === 0 ? 'leftArmRotation' : 'rightArmRotation'];

              const gamepad = gamepads[i];
              if (gamepad.visible) {
                const armQuaternionInverse = localQuaternion2.setFromRotationMatrix(
                  localMatrix.lookAt(
                    mesh.arms[i === 0 ? 'left' : 'right']
                      .getWorldPosition(localVector),
                    gamepad.position,
                    localVector2
                      .set(0, 1, 0)
                      .applyQuaternion(gamepad.quaternion)
                  )
                )
                  .multiply(armQuaternionOffset)
                  .premultiply(playerQuaternionInverse)
                  .inverse();
                armRotation.value.set(armQuaternionInverse.x, armQuaternionInverse.y, armQuaternionInverse.z, armQuaternionInverse.w);
              } else {
                armRotation.value.set(0, 0, 0, 1);
              }
            }
          };

          if (object.skinMesh) {
            object.skinMesh.parent.remove(object.skinMesh);
            object.skinMesh = null;
          }

          scene.add(mesh);
          object.skinMesh = mesh;
        });
    } else {
      onupdate = null;

      if (object.skinMesh) {
        object.skinMesh.parent.remove(object.skinMesh);
        object.skinMesh = null;
      }
    }
  };
  object.update = (hmd, gamepads) => {
    onupdate && onupdate(hmd, gamepads);
  };
  object.animate = thetaValue => {
    uniforms.theta.value = thetaValue;
  };
  object.destroy = () => {
    onupdate = null;

    if (object.skinMesh) {
      object.skinMesh.parent.remove(object.skinMesh);
      object.skinMesh = null;
    }
  };
  object.skinMesh = null;
  return object;
};
/* const localSkinMesh = _makeSkinMesh();
localSkinMesh.setSkinUrl(DEFAULT_SKIN_URL) */

const _makePositionSnapshotter = () => {
  const numSnapshots = 8;
  const positionShapshots = Array(numSnapshots);
  for (let i = 0; i < positionShapshots.length; i++) {
    positionShapshots[i] = {
      position: new THREE.Vector3(),
      timestamp: 0,
    };
  }
  let positionShapshotIndex = 0;
  let positionShapshotsInitialized = false;

  return {
    capturePositionSnapshot(position, timestamp) {
      const snapshot = positionShapshots[positionShapshotIndex];
      snapshot.position.copy(position);
      snapshot.timestamp = timestamp;

      positionShapshotIndex = (positionShapshotIndex + 1) % positionShapshots.length;
    },
    getFirstPosition() {
      return positionShapshots[positionShapshotIndex];
    },
    getLastPosition() {
      let index = positionShapshotIndex - 1;
      if (index < 0) {
        index += positionShapshots.length;
      }
      return positionShapshots[index];
    },
  };
};

// helpers

const _makeRenderer = () => {
  const scene = new THREE.Scene();
  // scene.background = new THREE.Color(0x7E57C2);
  scene.matrixAutoUpdate = false;

  const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 100);
  camera.position.y = 1.6;
  // camera.position.z = 1;

  const renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setSize( window.innerWidth, window.innerHeight );
  document.body.appendChild(renderer.domElement);

  return {
    scene,
    camera,
    renderer,
    destroy() {
      document.body.removeChild(renderer.domElement);
    },
  };
};
const _getFrontOfCamera = () => {
  const vrCamera = renderer.vr.enabled ? renderer.vr.getCamera(camera).cameras[0] : camera;
  vrCamera.matrixWorld.decompose(localVector, localQuaternion, localVector2);
  localVector.add(
    localVector2.set(0, 0, -1.5)
      .applyQuaternion(localQuaternion)
  );
  localVector2.set(1, 1, 1);

  return {
    position: localVector.clone(),
    quaternion: localQuaternion.clone(),
    scale: localVector2.clone(),
  };
};

const _openUrl = (u, position = new THREE.Vector3(), orientation = new THREE.Quaternion(), scale, d = 3, local = true, id = tabId++) => {
  if (scale === undefined) {
    scale = new THREE.Vector3(1, d === 3 ? 1 : window.innerHeight/window.innerWidth, 1);
    if (d !== 3) {
      scale.multiplyScalar(0.4);
    }
  }

  const _drawOk = () => {
    console.log('reality tab load ok: ' + u);
  };
  const _drawFail = () => {
    console.log('reality tab load error: ' + u);
  };

  const iframe = document.createElement('iframe');
  iframe.onconsole = (jsString, scriptUrl, startLine) => {
    console.log('parent got console', {jsString, scriptUrl, startLine});
  };
  iframe.onload = function() {
    const contentDocument = (() => {
      try {
        if (this.contentDocument) { // this potentially throws
          return this.contentDocument;
        } else {
          return null;
        }
      } catch(err) {
        console.warn(err.stack);
        return null;
      }
    })();
    if (contentDocument) {
      _drawOk();

      // scene.background = null;
    } else {
      _drawFail();

      _closeTab(tab);

      if (focusedTab === tab) {
        focusedTab = rig.menuMesh.urlMesh;
      }

      rig.menuMesh.urlMesh.updateText();
      _updateRigLists();
    }
  };
  iframe.d = d;
  iframe.src = u;
  /* iframe.addEventListener('destroy', () => {
    // scene.background = _makeBackground();
  }); */

  const tab = _addTab(iframe, position, orientation, scale, d, local, id);
};
const _addTab = (iframe, position = new THREE.Vector3(), orientation = new THREE.Quaternion(), scale, d = 3, local = true, id = tabId++) => {
  if (scale === undefined) {
    scale = new THREE.Vector3(1, d === 3 ? 1 : window.innerHeight/window.innerWidth, 1);
    if (d !== 3) {
      scale.multiplyScalar(0.4);
    }
  }

  iframe.position = position.toArray();
  iframe.orientation = orientation.toArray();
  iframe.scale = scale.toArray();

  document.body.appendChild(iframe);

  const tab = {
    url: iframe.src,
    id,
    iframe,
  };
  tabs.push(tab);
  layers.push(iframe);

  focusedTab = tab;

  rig.menuMesh.urlMesh.updateText();
  rig.menuMesh.listMesh.updateList();

  if (serverConnectedUrl && local) {
    const objectMesh = xrmp.createObjectMesh(tab.id, {
      url: tab.url,
      d,
    });
    const {object} = objectMesh;
    position.toArray(object.objectMatrix.position);
    orientation.toArray(object.objectMatrix.quaternion);
    object.pushUpdate();
    _bindObject(object);
  }

  return tab;
};
const _closeTab = tab => {
  const {id, iframe} = tab;
  if (iframe.destroy) {
    iframe.destroy();
  }
  document.body.removeChild(iframe);

  tabs.splice(tabs.indexOf(tab), 1);
  layers.splice(layers.indexOf(iframe), 1);

  if (serverConnectedUrl) {
    const objectMesh = xrmp.getObjectMeshes().find(objectMesh => objectMesh.object.id === id);
    xrmp.removeObjectMesh(objectMesh);
  }
};
const _closeAllTabs = () => { // XXX trigger this when switching servers
  for (let i = 0; i < tabs.length; i++) {
    const {iframe} = tab;
    if (iframe.destroy) {
      iframe.destroy();
    }
    document.body.removeChild(iframe);
  }

  tabs.length = 0;
  layers.length = 0;
};
const _openServer = (serverUrl, host) => {
  xrmp = new XRMultiplayerTHREE(new XRMultiplayer(serverUrl));
  xrmp.localPlayerMesh = null;
  xrmp.onopen = () => {
    xrmp.localPlayerMesh = xrmp.createLocalPlayerMesh(undefined, {});

    if (host) {
      for (let i = 0; i < tabs.length; i++) {
        const tab = tabs[i];
        const {url, iframe: {xrOffset, d}} = tab;
        const objectMesh = xrmp.createObjectMesh(tab.id, {
          url,
          d,
        });
        const {object} = objectMesh;
        object.objectMatrix.position.set(xrOffset.position);
        object.objectMatrix.quaternion.set(xrOffset.orientation);
        object.pushUpdate();
        _bindObject(object);
      }
    }
  };
  xrmp.onsync = () => {
    console.log('server sync', serverUrl);

    serverConnectedUrl = serverUrl;
    rig.menuMesh.listMesh.updateList();
  };
  xrmp.onclose = () => {
    console.log('server close', serverUrl);

    xrmp = null;
    serverConnectedUrl = null;
    rig.menuMesh.listMesh.updateList();
  };
  xrmp.onobjectadd = objectMesh => {
    const {object} = objectMesh;
    const {id, state} = object;
    if (!tabs.some(tab => tab.id === id)) {
      const {url, d} = state;
      _openUrl(url, undefined, undefined, undefined, d, false, id);
      _bindObject(object);
      console.log('server object add', id);
    } else {
      console.warn('ignoring duplicate server tab', id);
    }
  };
  xrmp.onobjectremove = objectMesh => {
    const {id} = objectMesh.object;
    const index = tabs.findIndex(tab => tab.id === id);
    if (index !== -1) {
      tabs.splice(index, 1);
      console.log('server object remove', id);
    } else {
      console.warn('failed to remove local server tab', id);;
    }
  };
  xrmp.onplayerenter = playerMesh => {
    const skinMesh = _makeSkinMesh();
    skinMesh.setSkinUrl(DEFAULT_SKIN_URL);
    playerMesh.skinMesh = skinMesh;

    const positionSnapshotter = _makePositionSnapshotter();
    playerMesh.onupdate = () => {
      skinMesh.update(playerMesh.hmd, playerMesh.gamepads);

      localVector.copy(playerMesh.hmd.position);
      localVector.y = 0;
      positionSnapshotter.capturePositionSnapshot(localVector, Date.now());
    };
    playerMesh.animate = () => {
      const walkRate = 1000;
      const f = (Date.now() % walkRate) / walkRate;
      const firstPosition = positionSnapshotter.getFirstPosition();
      const lastPosition = positionSnapshotter.getLastPosition();
      const walkSpeed = localVector.copy(firstPosition.position)
        .distanceTo(lastPosition.position) / (lastPosition.timestamp - firstPosition.timestamp);
      const walkIntensity = Math.min(walkSpeed * 1000, 1);
      const thetaValue = Math.sin(f * Math.PI*2) * walkIntensity * 0.7;
      skinMesh.animate(thetaValue);
    };

    scene.add(playerMesh);
  };
  xrmp.onplayerleave = playerMesh => {
    scene.remove(playerMesh);

    playerMesh.skinMesh.destroy();
  };
  xrmp.onerror = e => {
    console.warn(e.error);
  };
};
const _closeServer = () => {
  xrmp.onopen = null;
  xrmp.onsync = null;
  xrmp.onobjectadd = null;
  xrmp.onobjectremove = null;
  xrmp.close();
};
const _bindObject = o => {
  o.onupdate = e => {
    const {object, matrix} = e;
    const {id} = object;
    const tab = tabs.find(tab => tab.id === id);
    const {iframe} = tab;
    iframe.position = Array.from(matrix.position);
    iframe.orientation = Array.from(matrix.quaternion);
  };
};
const _updateRigLists = () => {
  const {menuMesh} = rig;
  if (menuMesh.listMesh.scrollIndex < 0) {
    menuMesh.listMesh.scrollIndex = 0;
  } else {
    const items = menuMesh.optionsMesh.list === 'tabs' ? tabs : links;
    if (menuMesh.listMesh.scrollIndex > items.length - 1) {
      menuMesh.listMesh.scrollIndex = items.length - 1;
    }
  }
  menuMesh.listMesh.updateList();
};

// main

const {scene, camera, renderer, destroy} = _makeRenderer();
const layers = [renderer.domElement];

const ambientLight = new THREE.AmbientLight(0x808080);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);

// controller meshes

const controllerGeometry = new THREE.BoxBufferGeometry(0.05, 0.1, 0.01);
const controllerMaterial = new THREE.MeshPhongMaterial({
  color: 0x4caf50,
});
const controllerMeshes = Array(2);
for (let i = 0; i < controllerMeshes.length; i++) {
  const controllerMesh = new THREE.Object3D();
  controllerMesh.position.set(i === 0 ? -0.1 : 0.1, 0, -0.1);
  controllerMesh.quaternion.setFromUnitVectors(
    new THREE.Vector3(0, 0, -1),
    new THREE.Vector3(0, -1, -1)
  );

  const rayMesh = (() => {
    const geometry = new THREE.CylinderBufferGeometry(0.001, 0.001, 1, 32, 1)
      .applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI / 2))
      .applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, -0.5));
    const material = new THREE.MeshBasicMaterial({
      color: RAY_COLOR,
    });

    const mesh = new THREE.Mesh(geometry, material);
    // mesh.visible = true;
    mesh.frustumCulled = false;
    return mesh;
  })();
  controllerMesh.add(rayMesh);
  controllerMesh.rayMesh = rayMesh;

  /* const dotMesh = (() => {
    const geometry = new THREE.SphereBufferGeometry(0.01, 5, 5);
    const material = new THREE.MeshBasicMaterial({
      color: 0xe91e63,
    });

    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.z = -1;
    mesh.visible = true;
    return mesh;
  })();
  controllerMesh.add(dotMesh);
  controllerMesh.dotMesh = dotMesh; */

  controllerMesh.ray = new THREE.Ray();
  controllerMesh.update = () => {
    controllerMesh.ray.origin.copy(controllerMesh.position);
    controllerMesh.ray.direction
      .set(0, 0, -1)
      .applyQuaternion(controllerMesh.quaternion);
  };

  scene.add(controllerMesh);
  controllerMeshes[i] = controllerMesh;
}
const _getControllerIndex = gamepad => gamepad.hand === 'left' ? 0 : 1;

// rig

let focusedTab = null;
const _makeRig = () => {
  const rig = new THREE.Object3D();
  rig.scale.set(0, 0, 0);
  rig.visible = false;
  rig.open = false;
  rig.animation = null;

  const menuMesh = (() => {
    const object = new THREE.Object3D();
    object.position.set(0, menuPositionHeight, 0);

    const urlMesh = (() => {
      const canvas = document.createElement('canvas');
      canvas.width = menuWidth;
      canvas.height = menuHeight;
      const ctx = canvas.getContext('2d');

      const geometry = new THREE.PlaneBufferGeometry(menuWorldWidth, menuWorldHeight);
      const texture = new THREE.Texture(
        canvas,
        THREE.UVMapping,
        THREE.ClampToEdgeWrapping,
        THREE.ClampToEdgeWrapping,
        THREE.LinearMipMapLinearFilter,
        THREE.LinearFilter,
        THREE.RGBAFormat,
        THREE.UnsignedByteType,
        16
      );
      texture.needsUpdate = true;
      const material = new THREE.MeshBasicMaterial({
        map: texture,
        side: THREE.DoubleSide,
        transparent: true,
        alphaTest: 0.9,
      });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.updateMatrixWorld();
      mesh.frustumCulled = false;

      focusedTab = mesh;

      const text = DEFAULT_URL;
      const urlState = {
        text,
        cursor: text.length,
        measures: [],
      };
      mesh.urlState = urlState;

      const _updateText = () => {
        ctx.fillStyle = '#EEE';
        ctx.fillRect(0, 0, menuWidth*0.8 - 5, menuHeight);

        if (focusedTab === mesh) {
          ctx.fillStyle = '#42a5f5';
          ctx.fillRect(0, menuHeight - 5, menuWidth*0.8 - 5, 5);
        }

        ctx.fillStyle = '#EEE';
        ctx.fillRect(menuWidth*0.8, 0, menuWidth*0.2 - 5, menuHeight);
        ctx.fillRect(0, menuHeight, menuWidth*0.5, menuHeight);

        ctx.fillStyle = '#000';
        ctx.font = `${fontSize}px ${fontFamily}`;

        ctx.fillText(urlState.text, 10, urlBarHeight - 10*2 - 5);
        ctx.fillText('x', menuWidth*0.88, menuHeight*0.7);

        urlState.measures.length = 0;
        urlState.measures.push(0);
        const {width: barWidth} = ctx.measureText('[');
        for (let i = 1; i <= urlState.text.length; i++) {
          const {width} = ctx.measureText('[' + urlState.text.slice(0, i) + ']');
          urlState.measures.push(width - barWidth*2);
        }

        ctx.fillStyle = '#000';
        ctx.fillRect(10 + urlState.measures[urlState.cursor] - cursorWidth/2, 20, cursorWidth, urlBarHeight - 20*2);

        texture.needsUpdate = true;
      };
      _updateText();
      mesh.updateText = _updateText;

      const _handleDefaultKey = (code, shiftKey) => {
        let c = String.fromCharCode(code);
        if (!shiftKey) {
          c = c.toLowerCase();
        }
        _handleSingleKey(c);
      };
      const _handleSingleKey = c => {
        mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor) + c + mesh.urlState.text.slice(mesh.urlState.cursor);
        mesh.urlState.cursor++;
        mesh.updateText();
      };
      const _handleKey = (code, shiftKey) => {
        if (code === 8) { // backspace
          if (mesh.urlState.cursor > 0) {
            mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor - 1) + mesh.urlState.text.slice(mesh.urlState.cursor);
            mesh.urlState.cursor--;
            mesh.updateText();
          } else {
            if (mesh.urlState.text.length === 0) {
              _closeRig();
            }
          }
        } else if (code === 46) { // delete
          if (mesh.urlState.cursor < mesh.urlState.text.length) {
            mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor) + mesh.urlState.text.slice(mesh.urlState.cursor + 1);
            _updateText();
          }
        } else if (code === 32) { // space
          mesh.urlState.text = mesh.urlState.text.slice(0, mesh.urlState.cursor) + ' ' + mesh.urlState.text.slice(mesh.urlState.cursor);
          mesh.urlState.cursor++;
          _updateText();
        } else if (code === 13) { // enter
          const u = (optionsMesh.d === 3 && urlMesh.urlState.text === DEFAULT_URL) ? 'realitytab.html?color=29b6f6' : urlMesh.urlState.text;
          const {position, quaternion, scale} = _getFrontOfCamera();
          _openUrl(u, position, quaternion, scale, optionsMesh.d);
          _updateRigLists();
        } else if (
          code === 9 || // tab
          code === 16 || // shift
          code === 17 || // ctrl
          code === 18 || // alt
          code === 20 || // capslock
          code === 27 || // esc
          code === 91 // win
        ) {
          // nothing
        } else if (code === 37) { // left
          mesh.urlState.cursor = Math.max(mesh.urlState.cursor - 1, 0);
          _updateText();
        } else if (code === 39) { // right
          mesh.urlState.cursor = Math.min(mesh.urlState.cursor + 1, mesh.urlState.text.length);
          _updateText();
        } else if (code === 38) { // up
          if (focusedTab === urlMesh) {
            // nothing
          } else {
            let tabIndex = tabs.indexOf(focusedTab);
            if (tabIndex === 0) {
              focusedTab = urlMesh;
            } else {
              tabIndex--;
              focusedTab = tabs[tabIndex];

              if (tabIndex < listMesh.scrollIndex) {
                listMesh.scrollIndex = tabIndex;
              } else if (tabIndex > listMesh.scrollIndex + 4) {
                listMesh.scrollIndex = tabIndex - 4;
              }
            }
          }
          _updateText();
          listMesh.updateList();
        } else if (code === 40) { // down
          if (focusedTab === urlMesh) {
            if (tabs.length > 0) {
              focusedTab = tabs[0];
              listMesh.scrollIndex = 0;
            } else {
              // nothing
            }
          } else {
            let tabIndex = tabs.indexOf(focusedTab);
            if (tabIndex === tabs.length - 1) {
              // nothing
            } else {
              tabIndex++;
              focusedTab = tabs[tabIndex];

              if (tabIndex < listMesh.scrollIndex) {
                listMesh.scrollIndex = tabIndex;
              } else if (tabIndex > listMesh.scrollIndex + 4) {
                listMesh.scrollIndex = tabIndex - 4;
              }
            }
          }
          _updateText();
          listMesh.updateList();
        } else if (code === 190) { // .
          _handleSingleKey('.');
        } else if (code === 188) { // ,
          _handleSingleKey(',');
        } else if (code === 191) { // /
          _handleSingleKey('/');
        } else if (code === 186) { // ;
          _handleSingleKey(';');
        } else if (code === 222) { // ;
          _handleSingleKey('\'');
        } else if (code === 219) { // [
          _handleSingleKey('[');
        } else if (code === 221) { // ]
          _handleSingleKey(']');
        } else if (code === 189) { // -
          _handleSingleKey('-');
        } else if (code === 187) { // =
          _handleSingleKey('=');
        } else if (code === 220) { // \
          _handleSingleKey('\\');
        } else if (code === -1) {
          // nothing
        } else {
          _handleDefaultKey(code, shiftKey);
        }
      };
      mesh.handleKey = _handleKey;

      mesh.plane = new THREE.Plane();
      mesh.leftLine = new THREE.Line3();
      mesh.topLine = new THREE.Line3();
      mesh.update = () => {
        mesh.leftLine.start
          .set(-menuWorldWidth/2, menuWorldHeight/2, 0)
          .applyMatrix4(mesh.matrixWorld);
        mesh.leftLine.end
          .set(-menuWorldWidth/2, -menuWorldHeight/2, 0)
          .applyMatrix4(mesh.matrixWorld);

        mesh.topLine.start
          .set(-menuWorldWidth/2, menuWorldHeight/2, 0)
          .applyMatrix4(mesh.matrixWorld);
        mesh.topLine.end
          .set(menuWorldWidth/2, menuWorldHeight / 2, 0)
          .applyMatrix4(mesh.matrixWorld);

        mesh.plane.setFromCoplanarPoints(
          mesh.leftLine.start,
          mesh.leftLine.end,
          mesh.topLine.end
        );
      };

      return mesh;
    })();
    object.add(urlMesh);
    object.urlMesh = urlMesh;

    const optionsMesh = (() => {
      const canvas = document.createElement('canvas');
      canvas.width = menuWidth;
      canvas.height = menuHeight;
      const ctx = canvas.getContext('2d');

      const geometry = new THREE.PlaneBufferGeometry(menuWorldWidth, menuWorldHeight);
      const texture = new THREE.Texture(
        canvas,
        THREE.UVMapping,
        THREE.ClampToEdgeWrapping,
        THREE.ClampToEdgeWrapping,
        THREE.LinearMipMapLinearFilter,
        THREE.LinearFilter,
        THREE.RGBAFormat,
        THREE.UnsignedByteType,
        16
      );
      texture.needsUpdate = true;
      const material = new THREE.MeshBasicMaterial({
        map: texture,
        side: THREE.DoubleSide,
        transparent: true,
        alphaTest: 0.9,
      });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.y = -menuWorldHeight - (10/menuHeight*menuWorldHeight);
      mesh.updateMatrixWorld();
      mesh.frustumCulled = false;
      mesh.d = 3;
      mesh.list = 'tabs';

      const _updateOptions = () => {
        ctx.font = `${fontSize}px ${fontFamily}`;

        // 3D
        ctx.fillStyle = mesh.d === 3 ? '#42a5f5' : '#EEE';
        ctx.fillRect(0, 0, menuWidth*0.2 - 5, menuHeight);
        ctx.fillStyle = mesh.d === 3 ? '#FFF' : '#000';
        ctx.fillText('3D', menuWidth*0.2/2 - 30, menuHeight/2 + 20);

        // 2D
        ctx.fillStyle = mesh.d === 2 ? '#42a5f5' : '#EEE';
        ctx.fillRect(menuWidth*0.2, 0, menuWidth*0.2 - 5, menuHeight);
        ctx.fillStyle = mesh.d === 2 ? '#FFF' : '#000';
        ctx.fillText('2D', menuWidth*0.2 + menuWidth*0.2/2 - 30, menuHeight/2 + 20);

        // Tabs
        ctx.fillStyle = mesh.list === 'tabs' ? '#42a5f5' : '#EEE';
        ctx.fillRect(menuWidth*0.4, 0, menuWidth*0.2 - 5, menuHeight);
        ctx.fillStyle = mesh.list === 'tabs' ? '#FFF' : '#000';
        ctx.fillText('Tabs', menuWidth*0.4 + menuWidth*0.2/2 - 70, menuHeight/2 + 20);

        // Links
        ctx.fillStyle = mesh.list === 'links' ? '#42a5f5' : '#EEE';
        ctx.fillRect(menuWidth*0.6, 0, menuWidth*0.2 - 5, menuHeight);
        ctx.fillStyle = mesh.list === 'links' ? '#FFF' : '#000';
        ctx.fillText('Links', menuWidth*0.6 + menuWidth*0.2/2 - 70, menuHeight/2 + 20);

        // Party
        ctx.fillStyle = mesh.list === 'party' ? '#42a5f5' : '#EEE';
        ctx.fillRect(menuWidth*0.8, 0, menuWidth*0.2 - 5, menuHeight);
        ctx.fillStyle = mesh.list === 'party' ? '#FFF' : '#000';
        ctx.fillText('Party', menuWidth*0.8 + menuWidth*0.2/2 - 70, menuHeight/2 + 20);

        texture.needsUpdate = true;
      };
      _updateOptions();
      mesh.updateOptions = _updateOptions;

      mesh.plane = new THREE.Plane();
      mesh.leftLine = new THREE.Line3();
      mesh.topLine = new THREE.Line3();
      mesh.update = () => {
        mesh.leftLine.start
          .set(-menuWorldWidth/2, menuWorldHeight/2, 0)
          .applyMatrix4(mesh.matrixWorld);
        mesh.leftLine.end
          .set(-menuWorldWidth/2, -menuWorldHeight/2, 0)
          .applyMatrix4(mesh.matrixWorld);

        mesh.topLine.start
          .set(-menuWorldWidth/2, menuWorldHeight/2, 0)
          .applyMatrix4(mesh.matrixWorld);
        mesh.topLine.end
          .set(menuWorldWidth/2, menuWorldHeight / 2, 0)
          .applyMatrix4(mesh.matrixWorld);

        mesh.plane.setFromCoplanarPoints(
          mesh.leftLine.start,
          mesh.leftLine.end,
          mesh.topLine.end
        );
      };

      return mesh;
    })();
    object.add(optionsMesh);
    object.optionsMesh = optionsMesh;

    const listMesh = (() => {
      const canvas = document.createElement('canvas');
      canvas.width = menuWidth;
      canvas.height = menuHeight*4;
      const ctx = canvas.getContext('2d');

      const geometry = new THREE.PlaneBufferGeometry(menuWorldWidth, menuWorldHeight*4);
      const texture = new THREE.Texture(
        canvas,
        THREE.UVMapping,
        THREE.ClampToEdgeWrapping,
        THREE.ClampToEdgeWrapping,
        THREE.LinearMipMapLinearFilter,
        THREE.LinearFilter,
        THREE.RGBAFormat,
        THREE.UnsignedByteType,
        16
      );
      texture.needsUpdate = true;
      const material = new THREE.MeshBasicMaterial({
        map: texture,
        side: THREE.DoubleSide,
        transparent: true,
        alphaTest: 0.9,
      });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.y = -menuWorldHeight*3.5 - (10/menuHeight*menuWorldHeight)*2;
      mesh.updateMatrixWorld();
      mesh.frustumCulled = false;
      mesh.scrollIndex = 0;
      mesh.scrollIndexFine = 0;

      const _updateList = () => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        ctx.font = `${fontSize}px ${fontFamily}`;

        const _renderScrollbar = listLength => {
          ctx.fillStyle = '#808080';
          ctx.fillRect(menuWidth*0.99, 0, menuWidth*0.01, menuHeight*4);
          const fullBarHeight = menuHeight*4;
          const barHeight = fullBarHeight/Math.min(listLength > 0 ? listLength : 1, 4);
          const barSpace = fullBarHeight - barHeight;
          ctx.fillStyle = '#42a5f5';
          ctx.fillRect(menuWidth*0.99, mesh.scrollIndex*barSpace/(listLength > 1 ? (listLength-1) : 1), menuWidth*0.01, barHeight);
        };

        for (let i = 0; i < iconMeshes.length; i++) {
          const iconMesh = iconMeshes[i];
          if (iconMesh.parent) {
            iconMesh.parent.remove(iconMesh);
          }
        }
        if (optionsMesh.list === 'tabs') {
          if (tabs.length > 0) {
            const localTabs = tabs.slice(mesh.scrollIndex, mesh.scrollIndex + 4);
            for (let i = 0; i < localTabs.length; i++) {
              const tab = localTabs[i];
              const {url} = tab;
              const y = menuHeight*i;

              ctx.fillStyle = tab === focusedTab ? '#2196f3' : '#FFF';
              ctx.fillRect(0, y, menuWidth*0.6 - 5, menuHeight);

              ctx.fillStyle = '#FFF';
              ctx.fillRect(menuWidth*0.6, y, menuWidth*0.1 - 5, menuHeight);
              ctx.fillRect(menuWidth*0.7, y, menuWidth*0.1 - 5, menuHeight);
              ctx.fillRect(menuWidth*0.8, y, menuWidth*0.18, menuHeight);

              ctx.fillStyle = tab === focusedTab ? '#FFF' : '#000';
              ctx.fillText(url, 100, y + menuHeight/2 + 20);

              ctx.fillStyle = '#000';
              ctx.strokeStyle = '#000';
              ctx.lineWidth = 3;
              ctx.beginPath();
              ctx.moveTo(menuWidth*0.65 - 5, y + menuHeight*0.5 - 20);
              ctx.lineTo(menuWidth*0.65 - 5, y + menuHeight*0.5 + 20);
              ctx.stroke();
              ctx.beginPath();
              ctx.moveTo(menuWidth*0.65 - 5 - 20, y + menuHeight*0.5);
              ctx.lineTo(menuWidth*0.65 - 5 + 20, y + menuHeight*0.5);
              ctx.stroke();

              ctx.beginPath();
              ctx.moveTo(menuWidth*0.75 - 5, y + menuHeight*0.5 - 20);
              ctx.lineTo(menuWidth*0.75 - 5 + 20, y + menuHeight*0.5);
              ctx.stroke();
              ctx.beginPath();
              ctx.moveTo(menuWidth*0.75 - 5 + 20, y + menuHeight*0.5);
              ctx.lineTo(menuWidth*0.75 - 5, y + menuHeight*0.5 + 20);
              ctx.stroke();

              ctx.fillText('x', menuWidth*0.88, y + menuHeight*0.7);
            }
          } else {
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, menuWidth*0.3 - 5, menuHeight);

            ctx.fillStyle = '#FFFFFF';
            ctx.fillText('No tabs', 50, menuHeight/2 + 20);
          }

          _renderScrollbar(tabs.length);
        } else if (optionsMesh.list === 'links') {
          const localLinks = links.slice(mesh.scrollIndex, mesh.scrollIndex + 4);
          for (let i = 0; i < localLinks.length; i++) {
            const link = localLinks[i];
            const {name} = link;
            const y = menuHeight*i;

            ctx.fillStyle = '#FFFFFF';
            ctx.fillRect(0, y, menuWidth*0.98, menuHeight);

            ctx.fillStyle = '#000000';
            ctx.fillText(name, 100, y + menuHeight/2 + 20);

            if (!link.iconMesh) {
              const iconMesh = (() => {
                const iconMesh = new THREE.Object3D();

                function pad(n, width) {
                  return n.length >= width ? n : new Array(width - n.length + 1).join('0') + n;
                }

                _requestSpriteMesh(`icons/Food_${pad((4+mesh.scrollIndex+i) + '', 2)}.png`)
                  .then(spriteMesh => {
                    spriteMesh.position.set(-0.45, 0.15 - i*0.1, 0.05);
                    spriteMesh.rotation.order = 'YXZ';
                    spriteMesh.scale.set(0.16, 0.16, 0.16);
                    spriteMesh.frustumCulled = false;
                    iconMesh.add(spriteMesh);
                  });

                return iconMesh;
              })();
              link.iconMesh = iconMesh;
              iconMeshes.push(iconMesh);
            }
            mesh.add(link.iconMesh);
            link.iconMesh.children[0] && (link.iconMesh.children[0].position.y = 0.15 - i*0.1);
          }

          _renderScrollbar(links.length);
        } else if (optionsMesh.list === 'party') {
          if (!serverConnectedUrl) {
            if (!serversLoading) {
              if (servers.length > 0) {
                const localServers = servers.slice(mesh.scrollIndex, mesh.scrollIndex + 4);
                for (let i = 0; i < localServers.length; i++) {
                  const server = localServers[i];
                  const {name} = server;
                  const y = menuHeight*i;

                  ctx.fillStyle = '#FFFFFF';
                  ctx.fillRect(0, y, menuWidth*0.98, menuHeight);

                  ctx.fillStyle = '#000000';
                  ctx.fillText(name, 100, y + menuHeight/2 + 20);

                  if (!server.iconMesh) {
                    const iconMesh = (() => {
                      const iconMesh = new THREE.Object3D();

                      function pad(n, width) {
                        return n.length >= width ? n : new Array(width - n.length + 1).join('0') + n;
                      }

                      _requestSpriteMesh(`icons/Food_${pad((4+mesh.scrollIndex+i) + '', 2)}.png`)
                        .then(spriteMesh => {
                          spriteMesh.position.set(-0.45, 0.15 - i*0.1, 0.05);
                          spriteMesh.rotation.order = 'YXZ';
                          spriteMesh.scale.set(0.16, 0.16, 0.16);
                          spriteMesh.frustumCulled = false;
                          iconMesh.add(spriteMesh);
                        });

                      return iconMesh;
                    })();
                    server.iconMesh = iconMesh;
                    iconMeshes.push(iconMesh);
                  }
                  mesh.add(server.iconMesh);
                  server.iconMesh.children[0] && (server.iconMesh.children[0].position.y = 0.15 - i*0.1);
                }
              } else {
                ctx.fillStyle = '#000000';
                ctx.fillRect(0, 0, menuWidth*0.99 - 5, menuHeight);

                ctx.fillStyle = '#FFFFFF';
                ctx.fillText('No servers', 50, menuHeight/2 + 20);
              }

              _renderScrollbar(servers.length);
            } else {
              ctx.fillStyle = '#000000';
              ctx.fillRect(0, 0, menuWidth*0.99 - 5, menuHeight);

              ctx.fillStyle = '#FFFFFF';
              ctx.fillText('Loading servers...', 50, menuHeight/2 + 20);

              _renderScrollbar(0);
            }
          } else {
            ctx.fillStyle = '#2196f3';
            ctx.fillRect(0, 0, menuWidth*0.8 - 5, menuHeight);
            ctx.fillStyle = '#FFF';
            ctx.fillRect(menuWidth*0.8, 0, menuWidth*0.18, menuHeight);

            ctx.fillStyle = '#FFF';
            ctx.fillText(serverConnectedUrl, 100, menuHeight/2 + 20);

            ctx.fillStyle = '#000';
            ctx.fillText('x', menuWidth*0.88, menuHeight*0.7);

            _renderScrollbar(0);
          }
        }

        texture.needsUpdate = true;
      };
      _updateList();
      mesh.updateList = _updateList;

      const _updateWheel = (dx, dy) => {
        mesh.scrollIndexFine += dy;
        while (mesh.scrollIndexFine >= 0) {
          mesh.scrollIndexFine--;
          mesh.scrollIndex--;
        }
        while (rig.menuMesh.listMesh.scrollIndexFine <= -1) {
          mesh.scrollIndexFine++;
          mesh.scrollIndex++;
        }
        _updateRigLists();
      };
      mesh.updateWheel = _updateWheel;

      mesh.plane = new THREE.Plane();
      mesh.leftLine = new THREE.Line3();
      mesh.topLine = new THREE.Line3();
      mesh.update = () => {
        mesh.leftLine.start
          .set(-menuWorldWidth/2, menuWorldHeight*4/2, 0)
          .applyMatrix4(mesh.matrixWorld);
        mesh.leftLine.end
          .set(-menuWorldWidth/2, -menuWorldHeight*4/2, 0)
          .applyMatrix4(mesh.matrixWorld);

        mesh.topLine.start
          .set(-menuWorldWidth/2, menuWorldHeight*4/2, 0)
          .applyMatrix4(mesh.matrixWorld);
        mesh.topLine.end
          .set(menuWorldWidth/2, menuWorldHeight*4/2, 0)
          .applyMatrix4(mesh.matrixWorld);

        mesh.plane.setFromCoplanarPoints(
          mesh.leftLine.start,
          mesh.leftLine.end,
          mesh.topLine.end
        );
      };

      return mesh;
    })();
    object.add(listMesh);
    object.listMesh = listMesh;

    return object;
  })();
  rig.add(menuMesh);
  rig.menuMesh = menuMesh;

  const keyboardMesh = (() => {
    const object = new THREE.Object3D();

    const planeMesh = (() => {
      const img = new Image();
      img.crossOrigin = 'Anonymous';
      img.src = 'keyboard.png';
      img.onload = () => {
        texture.needsUpdate = true;
      };
      img.onerror = err => {
        console.warn(err.stack);
      };

      const geometry = new THREE.PlaneBufferGeometry(1, keyboardHeight / keyboardWidth);
      const texture = new THREE.Texture(
        img,
        THREE.UVMapping,
        THREE.ClampToEdgeWrapping,
        THREE.ClampToEdgeWrapping,
        THREE.LinearMipMapLinearFilter,
        THREE.LinearFilter,
        THREE.RGBAFormat,
        THREE.UnsignedByteType,
        16
      );
      const material = new THREE.MeshBasicMaterial({
        map: texture,
        side: THREE.DoubleSide,
        transparent: true,
        alphaTest: 0.9,
      });

      const mesh = new THREE.Mesh(geometry, material);
      mesh.frustumCulled = false;
      mesh.position.y = -0.5;
      mesh.quaternion.setFromUnitVectors(
        new THREE.Vector3(0, 0, 1),
        new THREE.Vector3(0, 1, 1).normalize()
      );

      const keyMeshes = [];
      for (let i = 0; i < 2; i++) {
        const keyMesh = (() => {
          const geometry = new THREE.PlaneBufferGeometry(1, 1);
          const texture = new THREE.Texture(
            null,
            THREE.UVMapping,
            THREE.ClampToEdgeWrapping,
            THREE.ClampToEdgeWrapping,
            THREE.LinearMipMapLinearFilter,
            THREE.LinearFilter,
            THREE.RGBAFormat,
            THREE.UnsignedByteType,
            16
          );
          const material = new THREE.MeshBasicMaterial({
            map: texture,
            side: THREE.DoubleSide,
            transparent: true,
            alphaTest: 0.5,
          });
          const mesh = new THREE.Mesh(geometry, material);
          mesh.frustumCulled = false;
          return mesh;
        })();
        mesh.add(keyMesh);

        keyMeshes.push(keyMesh);
      }
      mesh.keyMeshes = keyMeshes;

      return mesh;
    })();
    object.add(planeMesh);
    object.planeMesh = planeMesh;

    object.plane = new THREE.Plane();
    object.leftLine = new THREE.Line3();
    object.topLine = new THREE.Line3();
    object.update = () => {
      object.leftLine.start
        .set(-1/2, keyboardHeight / keyboardWidth / 2, 0)
        .applyMatrix4(planeMesh.matrixWorld);
      object.leftLine.end
        .set(-1/2, -keyboardHeight / keyboardWidth / 2, 0)
        .applyMatrix4(planeMesh.matrixWorld);

      object.topLine.start
        .set(-1/2, keyboardHeight / keyboardWidth / 2, 0)
        .applyMatrix4(planeMesh.matrixWorld);
      object.topLine.end
        .set(1/2, keyboardHeight / keyboardWidth / 2, 0)
        .applyMatrix4(planeMesh.matrixWorld);

      object.plane.setFromCoplanarPoints(
        object.leftLine.start,
        object.leftLine.end,
        object.topLine.end
      );
    };

    return object;
  })();
  rig.add(keyboardMesh);
  rig.keyboardMesh = keyboardMesh;

  return rig;
};
const rig = _makeRig();
scene.add(rig);

const _openRig = (position, quaternion) => {
  if (!rig.open) {
    rig.position.copy(position);
    rig.quaternion.copy(quaternion);

    rig.open = true;
  } else {
    rig.open = false;
  }

  _animateRig();
};
const _closeRig = () => {
  rig.open = false;

  _animateRig();
};
const _animateRig = () => {
  const startScale = rig.scale.x;
  const endScale = rig.open ? 1 : 0;
  const startTime = Date.now();
  const endTime = startTime + 150;
  rig.animation = {
    update() {
      const now = Date.now();
      const factor = Math.min(Math.max(Math.pow((now - startTime) / (endTime - startTime), 0.2), 0), 1);
      const scaleFactor = startScale*(1-factor) + endScale*factor;

      rig.scale.set(scaleFactor, scaleFactor, scaleFactor);
      rig.visible = rig.scale.x > 0.00001;

      if (factor >= 1) {
        rig.animation = null;
      }
    },
  };
};

const cacheCanvas = document.createElement('canvas');
cacheCanvas.width = 1024;
cacheCanvas.height = 1024;
const cacheCanvasCtx = cacheCanvas.getContext('2d');

// box meshes

const boxMeshes = [
  _makeBoxMesh(),
  _makeBoxMesh(),
];
boxMeshes.forEach(boxMesh => {
  scene.add(boxMesh);
});

const moves = [null, null];

window.addEventListener('click', () => {
  if (fakeXrDisplay) {
    renderer.domElement.requestPointerLock();
  }
});
window.addEventListener('mousedown', e => {
  if (window.document.pointerLockElement) {
    const gamepads = navigator.getGamepads();
    gamepads[1].buttons[1].pressed = true;
    gamepads[1].buttons[1].value = 1;
  }
});
window.addEventListener('mouseup', e => {
  if (window.document.pointerLockElement) {
    const gamepads = navigator.getGamepads();
    gamepads[1].buttons[1].pressed = false;
    gamepads[1].buttons[1].value = 0;
  }
});
window.addEventListener('mousemove', e => {
  if (window.document.pointerLockElement) {
    const {movementX, movementY} = e;

    /* if (keys.ctrl) {
      const controllerOffset = controllerOffsets[controllerIndex];
      controllerOffset.x += movementX * 0.002;
      controllerOffset.y -= movementY * 0.002;
    } else if (keys.alt) {
      const controllerOffset = controllerOffsets[controllerIndex];
      controllerOffset.x += movementX * 0.002;
      controllerOffset.z += movementY * 0.002;
    } else { */
      localEuler.setFromQuaternion(fakeXrDisplay.quaternion, localEuler.order);
      localEuler.y -= movementX * 0.01;
      localEuler.x -= movementY * 0.01;
      localEuler.x = Math.min(Math.max(localEuler.x, -Math.PI/2), Math.PI/2);
      fakeXrDisplay.quaternion.setFromEuler(localEuler);

      fakeXrDisplay.pushUpdate();
    // }
  }
});
window.addEventListener('wheel', e => {
  if (window.document.pointerLockElement) {
    rig.menuMesh.listMesh.updateWheel(e.deltaX/100, -e.deltaY/100);
  }
});

const keys = {
  up: false,
  down: false,
  left: false,
  right: false,
};
const _makeButtons = () => ({
  trigger: false,
  bumper: false,
});
const lastButtons = [
  _makeButtons(),
  _makeButtons(),
];
const intersectionSpecs = [
  null,
  null,
];
window.addEventListener('keydown', e => {
  if (window.document.pointerLockElement) {
    const gamepads = navigator.getGamepads();

    switch (e.which) {
      case 87: { // W
        keys.up = true;
        /* if (!window.document.pointerLockElement) {
          renderer.domElement.requestPointerLock();
        } */
        break;
      }
      case 83: { // S
        keys.down = true;
        /* if (!window.document.pointerLockElement) {
          renderer.domElement.requestPointerLock();
        } */
        break;
      }
      case 65: { // A
        keys.left = true;
        /* if (!window.document.pointerLockElement) {
          renderer.domElement.requestPointerLock();
        } */
        break;
      }
      case 68: { // D
        keys.right = true;
        /* if (!window.document.pointerLockElement) {
          renderer.domElement.requestPointerLock();
        } */
        break;
      }
      case 69: { // E
        gamepads[1].buttons[2].pressed = true;
        break;
      }
      case 81: { // Q
        gamepads[1].buttons[0].pressed = true;
        break;
      }
      case 32: { // space
        keys.space = true;
        break;
      }
      case 17: { // ctrl
        keys.ctrl = true;
        break;
      }
    }
  } else {
    if (focusedTab) {
      if (focusedTab === rig.menuMesh.urlMesh) {
        rig.menuMesh.urlMesh.handleKey(e.keyCode, e.shiftKey);
      } else if (focusedTab.iframe) {
        focusedTab.iframe.sendKeyDown(e.which, {
          shiftKey: e.shiftKey,
          ctrlKey: e.ctrlKey,
          altKey: e.altKey,
        });
      }
    }
  }
});
window.addEventListener('keyup', e => {
  if (window.document.pointerLockElement) {
    const gamepads = navigator.getGamepads();

    switch (e.which) {
      case 87: { // W
        keys.up = false;
        break;
      }
      case 83: { // S
        keys.down = false;
        break;
      }
      case 65: { // A
        keys.left = false;
        break;
      }
      case 68: { // D
        keys.right = false;
        break;
      }
      case 69: { // E
        gamepads[1].buttons[2].pressed = false;
        break;
      }
      case 81: { // Q
        gamepads[1].buttons[0].pressed = false;
        break;
      }
      case 32: { // space
        keys.space = false;
        break;
      }
      case 17: { // ctrl
        keys.ctrl = false;
        break;
      }
    }
  } else {
    if (focusedTab && focusedTab.iframe) {
      focusedTab.iframe.sendKeyUp(e.which, {
        shiftKey: e.shiftKey,
        ctrlKey: e.ctrlKey,
        altKey: e.altKey,
      });
    }
  }
});
window.addEventListener('keypress', e => {
  if (!window.document.pointerLockElement) {
    if (focusedTab) {
      /* if (focusedTab === rig.menuMesh.urlMesh) {
        rig.menuMesh.urlMesh.handleKey(e.keyCode, e.shiftKey);
      } else */if (focusedTab.iframe) {
        focusedTab.iframe.sendKeyPress(e.which, {
          shiftKey: e.shiftKey,
          ctrlKey: e.ctrlKey,
          altKey: e.altKey,
        });
      }
    }
  }
});

// let renderTarget = null;
const startTime = Date.now();
let lastTime = 0;
function animate(time, frame) {
  const now = Date.now();
  const timeDiff = now - lastTime;
  const startTimeDiff = now - startTime;

  const gamepads = navigator.getGamepads();
  for (let i = 0; i < gamepads.length && i < 2; i++) {
    const gamepad = gamepads[i];
    const moveTab = moves[i];
    const boxMesh = boxMeshes[i];

    if (moveTab) {
      const {iframe} = moveTab;

      const scrollFactor = scrollFactors[i];
      const scaleFactor = scaleFactors[i];

      localVector.fromArray(gamepad.pose.position);
      localQuaternion.fromArray(gamepad.pose.orientation);
      localVector
        .add(
          localVector2.set(0, 0, -scrollFactor)
            .applyQuaternion(localQuaternion)
        );
      localVector2.set(scaleFactor, scaleFactor, scaleFactor);

      iframe.position = localVector.toArray();
      iframe.orientation = localQuaternion.toArray();
      iframe.scale = localVector2.toArray();

      boxMesh.position.copy(localVector);
      boxMesh.quaternion.copy(localQuaternion);
      boxMesh.scale.copy(localVector2);
      // boxMesh.updateMatrixWorld();
      boxMesh.visible = true;

      if (serverConnectedUrl) {
        const objectMesh = xrmp.getObjectMeshes().find(objectMesh => objectMesh.object.id === moveTab.id);
        const {object} = objectMesh;
        const {objectMatrix} = object;
        localVector.toArray(objectMatrix.position);
        localQuaternion.toArray(objectMatrix.quaternion);
        object.pushUpdate();
      }
    } else {
      boxMesh.visible = false;
    }
  }

  const _updateControllerMeshes = () => {
    if (renderer.vr.enabled) {
      for (let i = 0; i < controllerMeshes.length; i++) {
        controllerMeshes[i].visible = false;
      }

      for (let i = 0; i < gamepads.length && i < 2; i++) {
        const gamepad = gamepads[i];

        if (gamepad.connected) {
          const controllerIndex = _getControllerIndex(gamepad);
          const controllerMesh = controllerMeshes[controllerIndex];
          controllerMesh.position.fromArray(gamepad.pose.position);
          controllerMesh.quaternion.fromArray(gamepad.pose.orientation);
          controllerMesh.updateMatrixWorld(true);
          controllerMesh.visible = true;
        }
      }
    }
  };
  _updateControllerMeshes();

  const _updateIntersections = () => {
    for (let i = 0; i < 2; i++) {
      const controllerMesh = controllerMeshes[i];
      controllerMesh.update();

      const _setIntersectionDefault = () => {
        controllerMesh.rayMesh.scale.z = rayDistance;
        controllerMesh.rayMesh.updateMatrixWorld();

        rig.keyboardMesh.planeMesh.keyMeshes[i].visible = false;

        const gamepads = navigator.getGamepads();
        for (let i = 0; i < gamepads.length; i++) {
          const gamepad = gamepads[i];
          const pressed = gamepad.buttons[1].pressed;
          const lastPressed = lastPresseds[i];
          if (pressed && !lastPressed) {
            moves[i] = null;
          }
        }

        intersectionSpecs[i] = null;
      };

      const intersectionPoints = [];
      const intersectionCbs = [];
      const {keyboardMesh, menuMesh} = rig;
      const {planeMesh} = keyboardMesh;
      const keyMesh = planeMesh.keyMeshes[i];
      keyboardMesh.update();
      menuMesh.urlMesh.update();
      menuMesh.optionsMesh.update();
      menuMesh.listMesh.update();

      // keyboard
      {
        const intersectionPoint = keyboardMesh.visible ? controllerMesh.ray.intersectPlane(keyboardMesh.plane, localVector) : null;
        if (intersectionPoint) {
          const leftIntersectionPoint = keyboardMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);

          const topIntersectionPoint = keyboardMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);

          const xFactor = topIntersectionPoint.distanceTo(keyboardMesh.topLine.start) / (1);
          const yFactor = leftIntersectionPoint.distanceTo(keyboardMesh.leftLine.start) / (keyboardHeight / keyboardWidth);
          const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);

          if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
            const x = xFactor * keyboardWidth;
            const y = yFactor * keyboardHeight;

            intersectionPoints.push(intersectionPoint.clone());
            intersectionCbs.push(() => {
              controllerMesh.rayMesh.scale.z = distance;
              controllerMesh.updateMatrixWorld();

              /* controllerMesh.dotMesh.position.z = -distance;
              controllerMesh.updateMatrixWorld();
              controllerMesh.dotMesh.visible = true; */

              for (let j = 0; j < keyMap.length; j++) {
                const [key, kx1, ky1, kx2, ky2] = keyMap[j];
                if (x >= kx1 && x < kx2 && y >= ky1 && y < ky2) {
                  if (keyboardHighlightCanvasCtx) {
                    const width = kx2 - kx1;
                    const height = ky2 - ky1;
                    let imageData = keyboardHighlightCanvasCtx.getImageData(kx1, ky1, width, height);
                    if (key === 'enter') { // special case the enter key; it has a non-rectangular shape
                      cacheCanvasCtx.putImageData(imageData, 0, 0);
                      cacheCanvasCtx.clearRect(0, 0, 80, 140);

                      imageData = cacheCanvasCtx.getImageData(0, 0, imageData.width, imageData.height);
                    }

                    keyMesh.material.map.image = imageData;
                    keyMesh.material.map.needsUpdate = true;

                    keyMesh.position
                      .set(
                        -1/2 + ((width/2 + kx1) / keyboardWidth),
                        (keyboardHeight / keyboardWidth)/2 - ((height/2 + ky1) / keyboardHeight * (keyboardHeight / keyboardWidth)),
                         0.01 * (1)
                      );
                    keyMesh.scale.set(
                      width / keyboardWidth,
                      height / keyboardHeight * (keyboardHeight / keyboardWidth),
                      1
                    );
                    keyMesh.updateMatrixWorld();
                    keyMesh.visible = true;
                  }

                  intersectionSpecs[i] = {
                    type: 'key',
                    data: {
                      key,
                      rig,
                    },
                  };

                  break;
                }
              }
            });
          }
        }
      }

      // menu mesh
      {
        const intersectionPoint = menuMesh.visible ? controllerMesh.ray.intersectPlane(menuMesh.urlMesh.plane, localVector) : null;
        if (intersectionPoint) {
          const leftIntersectionPoint = menuMesh.urlMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);

          const topIntersectionPoint = menuMesh.urlMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);

          const xFactor = topIntersectionPoint.distanceTo(menuMesh.urlMesh.topLine.start) / menuWorldWidth;
          const yFactor = leftIntersectionPoint.distanceTo(menuMesh.urlMesh.leftLine.start) / menuWorldHeight;
          const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);

          if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
            const x = xFactor * menuWidth;
            const y = yFactor * menuHeight;

            if (y < urlBarOffset) {
              if (x < menuWidth*0.8) {
                intersectionPoints.push(intersectionPoint.clone());
                intersectionCbs.push(() => {
                  controllerMesh.rayMesh.scale.z = distance;
                  controllerMesh.updateMatrixWorld();

                  /* controllerMesh.dotMesh.position.z = -distance;
                  controllerMesh.updateMatrixWorld();
                  controllerMesh.dotMesh.visible = true; */

                  const gamepads = navigator.getGamepads();
                  const gamepad = gamepads[i];
                  if (gamepad.buttons[1].pressed) {
                    const textX = x - cursorWidth;
                    let closestIndex = -1;
                    let closestDistance = Infinity;
                    for (let i = 0; i < menuMesh.urlMesh.urlState.measures.length; i++) {
                      const urlMeasure = menuMesh.urlMesh.urlState.measures[i];
                      const distance = Math.abs(urlMeasure - textX);
                      if (distance < closestDistance) {
                        closestIndex = i;
                        closestDistance = distance;
                      }
                    }
                    if (closestIndex !== -1) {
                      menuMesh.urlMesh.urlState.cursor = closestIndex;
                    }

                    focusedTab = menuMesh.urlMesh;

                    menuMesh.urlMesh.updateText();
                    menuMesh.listMesh.updateList();
                  }

                  intersectionSpecs[i] = null;
                });
              } else {
                intersectionPoints.push(intersectionPoint.clone());
                intersectionCbs.push(() => {
                  controllerMesh.rayMesh.scale.z = distance;
                  controllerMesh.updateMatrixWorld();

                  /* controllerMesh.dotMesh.position.z = -distance;
                  controllerMesh.updateMatrixWorld();
                  controllerMesh.dotMesh.visible = true; */

                  const gamepads = navigator.getGamepads();
                  const gamepad = gamepads[i];
                  if (gamepad.buttons[1].pressed) {
                    _closeRig();
                  }

                  intersectionSpecs[i] = null;
                });
              }
            }
          }
        }
      }

      // options mesh
      {
        const intersectionPoint = menuMesh.visible ? controllerMesh.ray.intersectPlane(menuMesh.optionsMesh.plane, localVector) : null;
        if (intersectionPoint) {
          const leftIntersectionPoint = menuMesh.optionsMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);

          const topIntersectionPoint = menuMesh.optionsMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);

          const xFactor = topIntersectionPoint.distanceTo(menuMesh.optionsMesh.topLine.start) / menuWorldWidth;
          const yFactor = leftIntersectionPoint.distanceTo(menuMesh.optionsMesh.leftLine.start) / menuWorldHeight;
          const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);

          if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
            const x = xFactor * menuWidth;
            const y = yFactor * menuHeight;

            if (y >= 0 && y < urlBarOffset) {
              intersectionPoints.push(intersectionPoint.clone());
              intersectionCbs.push(() => {
                controllerMesh.rayMesh.scale.z = distance;
                controllerMesh.updateMatrixWorld();

                /* controllerMesh.dotMesh.position.z = -distance;
                controllerMesh.updateMatrixWorld();
                controllerMesh.dotMesh.visible = true; */

                const gamepads = navigator.getGamepads();
                const gamepad = gamepads[i];
                if (gamepad.buttons[1].pressed) {
                  const optionX = Math.floor(x/(menuWidth*0.2));
                  if (optionX === 0) {
                    menuMesh.optionsMesh.d = 3;
                    menuMesh.optionsMesh.updateOptions();
                  } else if (optionX === 1) {
                    menuMesh.optionsMesh.d = 2;
                    menuMesh.optionsMesh.updateOptions();
                  } else if (optionX === 2) {
                    menuMesh.optionsMesh.list = 'tabs';
                    menuMesh.optionsMesh.updateOptions();
                    menuMesh.listMesh.scrollIndex = 0;
                    menuMesh.listMesh.updateList();
                  } else if (optionX === 3) {
                    menuMesh.optionsMesh.list = 'links';
                    menuMesh.optionsMesh.updateOptions();
                    menuMesh.listMesh.scrollIndex = 0;
                    menuMesh.listMesh.updateList();
                  } else if (optionX === 4) {
                    menuMesh.optionsMesh.list = 'party';
                    if (!serversLoading) {
                      XRMultiplayer.requestServers(`${registryUrl}/servers`)
                        .then(result => {
                          servers = result.servers;
                          serversLoading = false;

                          menuMesh.listMesh.updateList();
                        })
                        .catch(err => {
                          console.warn(err.stack);

                          serversLoading = false;

                          menuMesh.listMesh.updateList();
                        });

                      serversLoading = true;
                    }
                    menuMesh.optionsMesh.updateOptions();
                    menuMesh.listMesh.scrollIndex = 0;
                    menuMesh.listMesh.updateList();
                  }
                }

                intersectionSpecs[i] = null;
              });
            }
          }
        }
      }

      // list mesh
      {
        const intersectionPoint = menuMesh.visible ? controllerMesh.ray.intersectPlane(menuMesh.listMesh.plane, localVector) : null;
        if (intersectionPoint) {
          const leftIntersectionPoint = menuMesh.listMesh.leftLine.closestPointToPoint(intersectionPoint, true, localVector2);

          const topIntersectionPoint = menuMesh.listMesh.topLine.closestPointToPoint(intersectionPoint, true, localVector3);

          const xFactor = topIntersectionPoint.distanceTo(menuMesh.listMesh.topLine.start) / menuWorldWidth;
          const yFactor = leftIntersectionPoint.distanceTo(menuMesh.listMesh.leftLine.start) / (menuWorldHeight*4);
          const distance = controllerMesh.ray.origin.distanceTo(intersectionPoint);

          if (xFactor > 0 && xFactor < 0.99999 && yFactor > 0 && yFactor < 0.99999 && distance < rayDistance) {
            const x = xFactor * menuWidth;
            const y = yFactor * menuHeight*4;

            if (x < menuWidth * 0.98) {
              if (menuMesh.optionsMesh.list === 'tabs') {
                const localTabs = tabs.slice(menuMesh.listMesh.scrollIndex, menuMesh.listMesh.scrollIndex + 4);
                for (let j = 0; j < localTabs.length; j++) {
                  if (y >= urlBarOffset*j && y < urlBarOffset*(j+1)) {
                    if (x >= menuWidth*0.8) {
                      intersectionPoints.push(intersectionPoint.clone());
                      intersectionCbs.push(() => {
                        controllerMesh.rayMesh.scale.z = distance;
                        controllerMesh.updateMatrixWorld();

                        /* controllerMesh.dotMesh.position.z = -distance;
                        controllerMesh.updateMatrixWorld();
                        controllerMesh.dotMesh.visible = true; */

                        const gamepads = navigator.getGamepads();
                        const gamepad = gamepads[i];
                        const pressed = gamepad.buttons[1].pressed;
                        const lastPressed = lastPresseds[i];
                        if (pressed && !lastPressed) {
                          const tab = tabs[menuMesh.listMesh.scrollIndex + j];
                          _closeTab(tab);

                          if (focusedTab === tab) {
                            focusedTab = rig.menuMesh.urlMesh;
                          }

                          rig.menuMesh.urlMesh.updateText();
                          _updateRigLists();
                        }

                        intersectionSpecs[i] = null;
                      });
                    } else if (x >= menuWidth*0.7) {
                      intersectionPoints.push(intersectionPoint.clone());
                      intersectionCbs.push(() => {
                        controllerMesh.rayMesh.scale.z = distance;
                        controllerMesh.updateMatrixWorld();

                        /* controllerMesh.dotMesh.position.z = -distance;
                        controllerMesh.updateMatrixWorld();
                        controllerMesh.dotMesh.visible = true; */

                        const gamepads = navigator.getGamepads();
                        const gamepad = gamepads[i];
                        const pressed = gamepad.buttons[1].pressed;
                        const lastPressed = lastPresseds[i];
                        if (pressed && !lastPressed) {
                          const tab = tabs[menuMesh.listMesh.scrollIndex + j];
                          const {iframe} = tab;
                          if (iframe.d === 3) {
                            window.browser.devTools.requestDevTools(iframe.contentWindow, document)
                              .then(devTools => {
                                const {position, quaternion, scale} = _getFrontOfCamera();
                                // position.add(localVector.set(0, -0.3, 0).applyQuaternion(quaternion));
                                _addTab(devTools.getIframe(), position, quaternion, scale, 2);
                              })
                              .catch(err => {
                                console.warn(err.stack);
                              });
                          }
                        }

                        intersectionSpecs[i] = null;
                      });
                    } else if (x >= menuWidth*0.6) {
                      intersectionPoints.push(intersectionPoint.clone());
                      intersectionCbs.push(() => {
                        controllerMesh.rayMesh.scale.z = distance;
                        controllerMesh.updateMatrixWorld();

                        /* controllerMesh.dotMesh.position.z = -distance;
                        controllerMesh.updateMatrixWorld();
                        controllerMesh.dotMesh.visible = true; */

                        const gamepads = navigator.getGamepads();
                        const gamepad = gamepads[i];
                        const pressed = gamepad.buttons[1].pressed;
                        const lastPressed = lastPresseds[i];
                        if (pressed && !lastPressed) {
                          const tab = tabs[menuMesh.listMesh.scrollIndex + j];
                          moves[i] = tab;

                          localVector.fromArray(gamepad.pose.position);
                          localQuaternion.fromArray(gamepad.pose.orientation);
                          const distance = localVector
                            .distanceTo(
                              localVector2
                                .fromArray(tab.iframe.xrOffset.position)
                            );
                          scrollFactors[i] = distance;

                          const scale = tab.iframe.xrOffset.scale[0];
                          scaleFactors[i] = scale;
                        }

                        intersectionSpecs[i] = null;
                      });
                    } else {
                      intersectionPoints.push(intersectionPoint.clone());
                      intersectionCbs.push(() => {
                        controllerMesh.rayMesh.scale.z = distance;
                        controllerMesh.updateMatrixWorld();

                        /* controllerMesh.dotMesh.position.z = -distance;
                        controllerMesh.updateMatrixWorld();
                        controllerMesh.dotMesh.visible = true; */

                        const gamepads = navigator.getGamepads();
                        const gamepad = gamepads[i];
                        const pressed = gamepad.buttons[1].pressed;
                        const lastPressed = lastPresseds[i];
                        if (pressed && !lastPressed) {
                          focusedTab = tabs[menuMesh.listMesh.scrollIndex + j];

                          menuMesh.urlMesh.updateText();
                          menuMesh.listMesh.updateList();
                        }

                        intersectionSpecs[i] = null;
                      });
                    }

                    break;
                  }
                }
              } else if (menuMesh.optionsMesh.list === 'links') {
                const localLinks = links.slice(menuMesh.listMesh.scrollIndex, menuMesh.listMesh.scrollIndex + 4);
                for (let j = 0; j < localLinks.length; j++) {
                  if (y >= urlBarOffset*j && y < urlBarOffset*(j+1)) {
                    intersectionPoints.push(intersectionPoint.clone());
                    intersectionCbs.push(() => {
                      controllerMesh.rayMesh.scale.z = distance;
                      controllerMesh.updateMatrixWorld();

                      /* controllerMesh.dotMesh.position.z = -distance;
                      controllerMesh.updateMatrixWorld();
                      controllerMesh.dotMesh.visible = true; */

                      const gamepads = navigator.getGamepads();
                      const gamepad = gamepads[i];
                      const pressed = gamepad.buttons[1].pressed;
                      const lastPressed = lastPresseds[i];
                      if (pressed && !lastPressed) {
                        const link = localLinks[j];
                        const {url} = link;

                        const {position, quaternion, scale} = _getFrontOfCamera();
                        _openUrl(url, position, quaternion, scale, menuMesh.optionsMesh.d);

                        menuMesh.optionsMesh.list = 'tabs';
                        menuMesh.optionsMesh.updateOptions();
                        menuMesh.listMesh.scrollIndex = 0;

                        _updateRigLists();
                      }

                      intersectionSpecs[i] = null;
                    });

                    break;
                  }
                }
              } else if (menuMesh.optionsMesh.list === 'party') {
                if (!serverConnectedUrl) {
                  const localServers = servers.slice(menuMesh.listMesh.scrollIndex, menuMesh.listMesh.scrollIndex + 4);
                  for (let j = 0; j < localServers.length; j++) {
                    if (y >= urlBarOffset*j && y < urlBarOffset*(j+1)) {
                      intersectionPoints.push(intersectionPoint.clone());
                      intersectionCbs.push(() => {
                        controllerMesh.rayMesh.scale.z = distance;
                        controllerMesh.updateMatrixWorld();

                        /* controllerMesh.dotMesh.position.z = -distance;
                        controllerMesh.updateMatrixWorld();
                        controllerMesh.dotMesh.visible = true; */

                        const gamepads = navigator.getGamepads();
                        const gamepad = gamepads[i];
                        const pressed = gamepad.buttons[1].pressed;
                        const lastPressed = lastPresseds[i];
                        if (pressed && !lastPressed) {
                          const server = localServers[j];
                          const {name} = server;

                          _openServer(`${registryUrl}/servers/${name}`, true);
                        }

                        intersectionSpecs[i] = null;
                      });

                      break;
                    }
                  }
                } else {
                  if (y >= 0 && y < urlBarOffset) {
                    if (x >= menuWidth*0.8) {
                      intersectionPoints.push(intersectionPoint.clone());
                      intersectionCbs.push(() => {
                        controllerMesh.rayMesh.scale.z = distance;
                        controllerMesh.updateMatrixWorld();

                        /* controllerMesh.dotMesh.position.z = -distance;
                        controllerMesh.updateMatrixWorld();
                        controllerMesh.dotMesh.visible = true; */

                        const gamepads = navigator.getGamepads();
                        const gamepad = gamepads[i];
                        const pressed = gamepad.buttons[1].pressed;
                        const lastPressed = lastPresseds[i];
                        if (pressed && !lastPressed) {
                          _closeServer();
                        }

                        intersectionSpecs[i] = null;
                      });
                    }
                  }
                }
              }
            } else {
              intersectionPoints.push(intersectionPoint.clone());
              intersectionCbs.push(() => {
                controllerMesh.rayMesh.scale.z = distance;
                controllerMesh.updateMatrixWorld();

                /* controllerMesh.dotMesh.position.z = -distance;
                controllerMesh.updateMatrixWorld();
                controllerMesh.dotMesh.visible = true; */

                const gamepads = navigator.getGamepads();
                const gamepad = gamepads[i];
                const pressed = gamepad.buttons[1].pressed;
                if (pressed) {
                  const list = menuMesh.optionsMesh.list === 'tabs' ? tabs : links;
                  menuMesh.listMesh.scrollIndex = Math.floor(yFactor*list.length);
                  if (menuMesh.listMesh.scrollIndex >= list.length) {
                    menuMesh.listMesh.scrollIndex = list.length;
                  }
                  menuMesh.listMesh.updateList();
                }

                intersectionSpecs[i] = null;
              });
            }
          }
        }
      }

      if (intersectionPoints.length > 0) {
        const intersectionCb = intersectionPoints
          .map((a, i) => ({
            distance: a.distanceTo(controllerMesh.position),
            cb: intersectionCbs[i],
          }))
          .sort((a, b) => a.distance - b.distance)[0].cb;
        intersectionCb();
      } else {
        _setIntersectionDefault();
      }

      const gamepads = navigator.getGamepads();
      const gamepad = gamepads[i];
      if (gamepad) {
        const pressed = gamepad.buttons[1].pressed;
        const padPressed = gamepad.buttons[0].pressed;
        const padTouched = gamepad.buttons[0].touched;

        const {axes} = gamepad;
        const lastPadTouched = lastPadToucheds[i];
        const lastAxis = lastAxes[i];
        if (padTouched && lastPadTouched && rig.open && !moves[i]) {
          const dx = axes[0] - lastAxis[0];
          const dy = axes[1] - lastAxis[1];

          rig.menuMesh.listMesh.updateWheel(dx*4, dy*4);
        }
        lastPresseds[i] = pressed;
        lastPadToucheds[i] = padTouched;

        if (padPressed && moves[i]) {
          if (padPressed) {
            if (gamepad.axes[1] > 0.5) {
              scrollFactors[i] += 1/20;
            } else if (gamepad.axes[1] < -0.5) {
              scrollFactors[i] -= 1/20;
            } else if (gamepad.axes[0] > 0.5) {
              scaleFactors[i] = Math.min(scaleFactors[i] *= 1.02, 100);
            } else if (gamepad.axes[0] < -0.5) {
              scaleFactors[i] = Math.max(scaleFactors[i] /= 1.02, 0.01);
            }
          }
        }

        lastAxis[0] = axes[0];
        lastAxis[1] = axes[1];
      }
    }
  };
  _updateIntersections();

  const _updateButtons = () => {
    for (let i = 0; i < gamepads.length && i < 2; i++) {
      const gamepad = gamepads[i];

      {
        const trigger = gamepad.buttons[1].pressed;
        const lastTrigger = lastButtons[i].trigger;

        if (trigger && !lastTrigger) {
          const spec = intersectionSpecs[i];
          if (spec && spec.type === 'key') {
            const {data: {key, rig}} = spec;
            const {menuMesh: {urlMesh, optionsMesh}} = rig;

            if (focusedTab === rig.menuMesh.urlMesh) {
              if (key !== 'enter') {
                const code = keyCode(key);
                rig.menuMesh.urlMesh.handleKey(code, false);
              } else {
                const u = (optionsMesh.d === 3 && urlMesh.urlState.text === DEFAULT_URL) ? 'realitytab.html?color=29b6f6' : urlMesh.urlState.text;
                const {position, quaternion, scale} = _getFrontOfCamera();
                _openUrl(u, position, quaternion, scale, optionsMesh.d);
                _updateRigLists();
              }
            } else if (focusedTab.iframe) {
              let code = keyCode(key);
              const opts = {
                shiftKey: false,
                ctrlKey: false,
                altKey: false,
              };
              focusedTab.iframe.sendKeyDown(code, opts);
              focusedTab.iframe.sendKeyPress(code + 32, opts);
              focusedTab.iframe.sendKeyUp(code, opts);
            }
          }
        }

        lastButtons[i].trigger = trigger;
      }
      {
        const bumper = gamepad.buttons[2].pressed;
        const lastBumper = lastButtons[i].bumper;

        if (bumper && !lastBumper) {
          const {position, quaternion} = _getFrontOfCamera();
          localEuler.setFromQuaternion(quaternion, localEuler.order);
          localEuler.x = 0;
          localEuler.z = 0;
          quaternion.setFromEuler(localEuler);
          _openRig(position, quaternion);
        }

        lastButtons[i].bumper = bumper;
      }
    }
  };
  _updateButtons();

  const _render = () => {
    renderer.render(scene, renderer.vr.enabled ? renderer.vr.getCamera(camera) : camera);
  };
  _render();

  const _updateMovement = () => {
    if (fakeXrDisplay) {
      // if (window.document.pointerLockElement) {
        // let moving = false;
        const speed = 0.004;
        const velocity = localVector.set(0, 0, 0);
        if (keys.up) {
          velocity.z -= speed * timeDiff;
          // moving = true;
        }
        if (keys.down) {
          velocity.z += speed * timeDiff;
          // moving = true;
        }
        if (keys.left) {
          velocity.x -= speed * timeDiff;
          // moving = true;
        }
        if (keys.right) {
          velocity.x += speed * timeDiff;
          // moving = true;
        }
        if (keys.space) {
          velocity.y += speed * timeDiff;
          // moving = true;
        }
        if (keys.ctrl) {
          velocity.y -= speed * timeDiff;
          // moving = true;
        }

        // if (moving) {
          // velocity.multiplyScalar(Math.pow(moving ? 0.98 : 0.91, timePassed / 5));
          localEuler.setFromQuaternion(fakeXrDisplay.quaternion, localEuler.order);
          localEuler.x = 0;
          fakeXrDisplay.position.add(
            localVector.copy(velocity)
              .applyEuler(localEuler)
          );
          fakeXrDisplay.pushUpdate();
        // }
      // }
    }
  };
  _updateMovement();

  const _updateRig = () => {
    if (rig.animation) {
      rig.animation.update();
    }

    const iconMeshRotation = (Date.now() % 1000) / 1000 * Math.PI * 2;
    for (let i = 0; i < iconMeshes.length; i++) {
      const iconMesh = iconMeshes[i];
      const child = iconMesh.children[0];
      if (child) {
        child.rotation.y = iconMeshRotation;
      }
    }
  };
  _updateRig();

  const _updateMp = () => {
    /* const vrCamera = renderer.vr.enabled ? renderer.vr.getCamera(camera).cameras[0] : camera;
    vrCamera.matrixWorld.decompose(localVector, localQuaternion, localVector2);
    localSkinMesh.update(vrCamera, controllerMeshes); */

    if (serverConnectedUrl) {
      const vrCamera = renderer.vr.enabled ? renderer.vr.getCamera(camera).cameras[0] : camera;
      vrCamera.matrixWorld.decompose(xrmp.localPlayerMesh.hmd.position, xrmp.localPlayerMesh.hmd.quaternion, localVector2);

      for (let i = 0; i < 2; i++) {
        const controllerMesh = controllerMeshes[i];
        xrmp.localPlayerMesh.gamepads[i].visible = true;
        xrmp.localPlayerMesh.gamepads[i].position.copy(controllerMesh.position);
        xrmp.localPlayerMesh.gamepads[i].quaternion.copy(controllerMesh.quaternion);
      }

      xrmp.pushUpdate();

      const playerMeshes = xrmp.getRemotePlayerMeshes();
      for (let i = 0; i < playerMeshes.length; i++) {
        playerMeshes[i].animate();
      }
    }
  };
  _updateMp();

  lastTime = now;
}

// bootstrap

const _bootFakeXrDisplay = async () => {
  fakeXrDisplay = _makeFakeXrDisplay();
  fakeXrDisplay.position.set(0, 1.6, 0);
  fakeXrDisplay.pushUpdate();

  await fakeXrDisplay.enter({
    renderer,
    animate,
    layers,
  });

  const tabUrl = (() => {
    const {t = 'examples/tutorial.html'} = query;
    if (!/^[a-z]+:\/\//.test(t)) {
      return `../${t}`;
    } else {
      return t;
    }
  })();
  _openUrl(tabUrl);

  focusedTab = rig.menuMesh.urlMesh;
  rig.menuMesh.urlMesh.updateText();
  rig.menuMesh.listMesh.updateList();

  console.log('loaded root in 2D');
};
(async () => {
  if (!query.fake) {
    if (navigator.xr) { // WebXR
      session = await navigator.xr.requestSession({
        exclusive: true,
      }).catch(err => Promise.resolve(null));
      if (session) {
        session.layers = layers;

        session.requestAnimationFrame((timestamp, frame) => {
          renderer.vr.setSession(session, {
            frameOfReferenceType: 'stage',
          });

          const {views} = frame.getViewerPose();
          const viewport = session.renderState.baseLayer.getViewport(views[0]);
          const height = viewport.height;
          const fullWidth = (() => {
            let result = 0;
            for (let i = 0; i < views.length; i++) {
              result += session.renderState.baseLayer.getViewport(views[i]).width;
            }
            return result;
          })();

          renderer.setSize(fullWidth, height);

          renderer.setAnimationLoop(null);

          renderer.vr.enabled = true;
          renderer.vr.setAnimationLoop(animate);

          /* if (window.browser && window.browser.magicleap) {
            window.browser.magicleap.RequestDepthPopulation(true);
            // renderer.autoClearDepth = false;
          } */

          _openUrl('tutorial.html');
          _updateRigLists();

          console.log('loaded in xr device');
        });
      } else {
        console.log('no xr devices');
        await _bootFakeXrDisplay();
      }
    } else { // WebVR
      console.log('request device');
      const displays = await navigator.getVRDisplays();
      display = displays[0];

      if (display) {
        console.log('request present');
        await display.requestPresent([
          {
            source: renderer.domElement,
          }
        ]);
        console.log('entered vr');

        display.layers = layers;

        const {renderWidth: width, renderHeight: height} = display.getEyeParameters('left');
        renderer.setSize(width * 2, height);

        renderer.setAnimationLoop(null);
        renderer.vr.enabled = true;
        renderer.vr.setDevice(display);
        renderer.vr.setAnimationLoop(animate);

        if (window.browser && window.browser.magicleap) {
          window.browser.magicleap.RequestDepthPopulation(true);
          // renderer.autoClearDepth = false;
        }

        _openUrl('tutorial.html');
        _updateRigLists();

        console.log('loaded root in XR');
      } else {
        console.log('no vr displays');
        await _bootFakeXrDisplay();
      }
    }
  } else {
    await _bootFakeXrDisplay();
  }
})()
  .catch(err => {
    console.warn(err);
  });

// renderer.setAnimationLoop(animate);
// _openRig(new THREE.Vector3(0, 1.5, -1), new THREE.Quaternion());
    </script>
  </body>
</html>
